今天要把「選中行星」這件事真正利用起來。
也就是點擊 Earth 之後 Camera 開始鎖定 Earth,OrbitControls.target 跟著 Earth 移動,
左下角顯示 Earth 資訊。
另外也會加入返回太陽系讓 Camera 回到以 Sun 為中心的模式。
由於目前專案程式碼已逐漸增加,從 Day 23 開始文章只列出當日新增或修改的程式碼。
完整整合版將於 Day 30 最終完成時提供。
今天會完成:
| 項目 | 修改內容 |
|---|---|
| Raycaster | 點擊 Planet 後儲存 selectedPlanet |
| Camera | 進入 Focus Mode |
| OrbitControls | target 每一幀更新到 Planet Position |
| UI | 新增 Planet Info Panel |
| Planet Data | 加入中文名稱、簡介等資訊 |
| Return Button | 返回 Sun Center Mode |
原本 planetData 只有:
{
name: "Earth",
size: 0.7,
distance: 8.5,
rotationSpeed: 1.0,
orbitSpeed: 0.8,
texture: "textures/earth.jpg"
}
今天可以多放一些資訊:
{
name: "Earth",
chineseName: "地球",
description: "目前已知唯一存在生命的行星",
order: "第三顆行星",
distance: 8.5,
size: 0.7,
rotationSpeed: 1.0,
orbitSpeed: 0.8,
texture: "textures/earth.jpg"
}
這些資料不一定要完全按照真實天文單位。
今天主要目的是 選到哪顆 Planet 就顯示哪顆 Planet 的資料
其他行星也照這個格式補上。
原本:
planets.push({
name:
data.name,
mesh:
planet,
distance:
data.distance,
rotationSpeed:
data.rotationSpeed,
orbitSpeed:
data.orbitSpeed,
angle:
startAngle
});
今天改成:
planets.push({
name:
data.name,
chineseName:
data.chineseName,
description:
data.description,
order:
data.order,
mesh:
planet,
size:
data.size,
distance:
data.distance,
rotationSpeed:
data.rotationSpeed,
orbitSpeed:
data.orbitSpeed,
angle:
startAngle
});
這樣 Raycaster 找到 Planet 後,就可以直接取得:
selectedPlanet.chineseName
selectedPlanet.description
selectedPlanet.order
Day 26 裡我們只是在 Click Event 裡暫時建立:
const selectedPlanet =
planets.find(...);
今天要讓這個 Planet 持續被記住。
所以改成:
let selectedPlanet =
null;
放在外層。
代表:
null → 沒有鎖定 Planet
Earth Object → 正在鎖定 Earth
Saturn Object → 正在鎖定 Saturn
再準備:
let focusMode =
false;
可以理解成:
false → Camera 以 Sun 為中心
true → Camera 跟隨 selectedPlanet
Day 26 原本:
if (
selectedPlanet
) {
highlightOrbit(
selectedPlanet.name
);
}
今天改成:
if (
clickedPlanet
) {
selectedPlanet =
clickedPlanet;
focusMode =
true;
highlightOrbit(
selectedPlanet.name
);
showPlanetInfo(
selectedPlanet
);
}
這裡我把 selectedPlanet 留給外層狀態使用。
所以 Click Event 裡暫時找到的物件改名,
clickedPlanet 比較不容易搞混。
點擊後不能直接:
camera.position.copy(
selectedPlanet.mesh.position
);
因為這樣 Camera 會跑到 Planet 裡面,
所以要保留一點距離。
例如:
const focusOffset =
new THREE.Vector3(
4,
2,
4
);
然後:
camera.position.copy(
selectedPlanet.mesh.position
);
camera.position.add(
focusOffset
);
這樣 Camera 就會在 Planet 附近,而不是重疊在同一個位置。
但 Jupiter 和 Mercury 大小不同。
如果全部使用 (4, 2, 4) Jupiter 可能太近,Mercury 又可能太遠。
所以可以根據 selectedPlanet.size 計算:
const focusDistance =
selectedPlanet.size *
6 +
3;
接著:
camera.position.set(
selectedPlanet.mesh.position.x +
focusDistance,
selectedPlanet.mesh.position.y +
focusDistance *
0.5,
selectedPlanet.mesh.position.z +
focusDistance
);
這樣:
大 Planet → Camera 稍微遠一點
小 Planet → Camera 稍微近一點
畫面會比較自然。
Planet 還在公轉
所以如果只在點擊時:
controls.target.copy(
selectedPlanet.mesh.position
);
下一秒 Planet 就已經跑走了。
會變成:
Camera Target → 留在舊位置
Planet → 繼續公轉離開
所以不能只設定一次。
在 animate() 裡加入:
if (
focusMode &&
selectedPlanet
) {
controls.target.copy(
selectedPlanet.mesh.position
);
}
這樣每一幀:
Earth 移動 → controls.target 更新 → Camera 持續看著 Earth
這就是 Planet Focus Mode 最重要的地方。
如果只有 controls.target.copy(...)
Camera 會一直看著 Planet。
但是 Planet 公轉越久,Camera 跟 Planet 的距離會逐漸改變。
所以更好的做法是保存:
const focusCameraOffset =
new THREE.Vector3();
當選擇 Planet 時:
focusCameraOffset
.copy(
camera.position
)
.sub(
selectedPlanet.mesh.position
);
意思就是 Camera Position - Planet Position = Camera 相對 Planet 的位置
之後每一幀:
camera.position.copy(
selectedPlanet.mesh.position
);
camera.position.add(
focusCameraOffset
);
這樣 Planet 移動多少,Camera 也跟著移動多少。
接著修改 index.html。
在 Control Panel 之外加入:
<div
class="planet-info"
id="planetInfo"
>
<h2 id="planetName">
Planet
</h2>
<p id="planetChineseName"></p>
<p id="planetOrder"></p>
<p id="planetDescription"></p>
<button id="returnButton">
返回太陽系
</button>
</div>
一開始不需要顯示。
加入:
.planet-info {
position: fixed;
left: 40px;
bottom: 40px;
z-index: 20;
width: 280px;
padding: 20px;
background:
rgba(
15,
15,
25,
0.82
);
border:
1px solid
rgba(
255,
255,
255,
0.15
);
border-radius: 12px;
backdrop-filter:
blur(10px);
display: none;
}
.planet-info.active {
display: block;
}
.planet-info h2 {
margin-top: 0;
}
.planet-info p {
margin: 8px 0;
color: #cccccc;
font-size: 14px;
line-height: 1.5;
}
.planet-info button {
width: 100%;
margin-top: 14px;
padding: 10px;
border: none;
border-radius: 8px;
background:
rgba(
255,
255,
255,
0.12
);
color: white;
cursor: pointer;
}
JavaScript
const planetInfo =
document.getElementById(
"planetInfo"
);
const planetName =
document.getElementById(
"planetName"
);
const planetChineseName =
document.getElementById(
"planetChineseName"
);
const planetOrder =
document.getElementById(
"planetOrder"
);
const planetDescription =
document.getElementById(
"planetDescription"
);
const returnButton =
document.getElementById(
"returnButton"
);
建立:
function showPlanetInfo(
planet
) {
planetName.textContent =
planet.name;
planetChineseName.textContent =
"中文名稱:" +
planet.chineseName;
planetOrder.textContent =
"位置:" +
planet.order;
planetDescription.textContent =
planet.description;
planetInfo.classList.add(
"active"
);
}
例如點 Earth:
Earth
中文名稱:地球
位置:第三顆行星
目前已知唯一存在生命的行星
今天可以把 Focus 邏輯整理成一個 Function:
function focusPlanet(
planet
) {
selectedPlanet =
planet;
focusMode =
true;
const focusDistance =
planet.size *
6 +
3;
camera.position.set(
planet.mesh.position.x +
focusDistance,
planet.mesh.position.y +
focusDistance *
0.5,
planet.mesh.position.z +
focusDistance
);
focusCameraOffset
.copy(
camera.position
)
.sub(
planet.mesh.position
);
controls.target.copy(
planet.mesh.position
);
controls.update();
showPlanetInfo(
planet
);
}
所以 Raycaster 選中後只需要:
focusPlanet(
clickedPlanet
);
接下來建立:
function returnToSolarSystem() {
focusMode =
false;
selectedPlanet =
null;
camera.position.set(
30,
25,
30
);
controls.target.set(
0,
0,
0
);
controls.update();
planetInfo.classList.remove(
"active"
);
resetOrbitColors();
}
按鈕:
returnButton.addEventListener(
"click",
returnToSolarSystem
);
這樣:
Planet Focus → 返回太陽系 → Camera 回 Sun
→ Info Panel 隱藏 → Orbit 恢復灰色
這裡還有一個小細節。
如果目前正在 Earth Focus
然後使用者按數字鍵,
我們應該讓 Focus Mode 解除。
所以 Keyboard Event 一開始加入:
focusMode =
false;
selectedPlanet =
null;
planetInfo.classList.remove(
"active"
);
這樣 Camera Shortcut 才不會下一幀又被 Earth 拉回去。
animate() 最後,在 controls.update() 之前加入:
if (
focusMode &&
selectedPlanet
) {
camera.position.copy(
selectedPlanet.mesh.position
);
camera.position.add(
focusCameraOffset
);
controls.target.copy(
selectedPlanet.mesh.position
);
}
因此:
Planet 公轉 → Planet Position 改變→ Camera Position 跟著移動
→ controls.target 跟著移動 → 畫面持續鎖定 Planet
今天最重要的不是單純 controls.target.copy(...)
而是要理解 Planet 正在移動,所以 Focus Mode 必須每一幀更新 Camera,
才能真的跟著 Planet。
現在使用者已經可以:
點 Earth
→ Camera 鎖定 Earth
→ Earth Orbit Highlight
→ 顯示 Earth Info
點 Saturn
→ Camera 鎖定 Saturn
→ Saturn Orbit Highlight
→ 顯示 Saturn Info
返回太陽系
→ 回到 Sun Center Mode
「Responsive × Performance:讓太陽系在不同裝置更順暢」
目前功能已經很多:
8 Planets
Moon
Venus Atmosphere
Saturn Ring
Starfield
Shadow
OrbitControls
UI
Animation Controls
Raycaster
Planet Focus
Day 28 要開始整理效能。
我們會處理 不同螢幕尺寸和 UI Responsive
高 DPI 螢幕 → Pixel Ratio 限制
大量 Geometry → 降低不必要 Segment
Shadow → 只保留必要物件
手機 / 小螢幕 → 調整 UI
為最後 Day 29 的整合 Debug,以及 Day 30 最終作品做準備。